package com.netflexitysolutions.amazonws.sdb.orm.metadata;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.netflexitysolutions.amazonws.sdb.orm.internal.util.AmazonSDBDataUtils;

/**
 * @author Max Fedorov
 *
 * Contains all the objects and attributes that belong to 
 * &lt;property&gt; XML element.
 */
public class AttributeMetadata extends AbstractSimpleDBMetadata {

    private static Log logger = LogFactory.getLog(AttributeMetadata.class.getPackage().getName());
    
    // Values for normalization bit mask.
    public static final int LOWER_CASE = 1;
    public static final int UPPER_CASE = 2;
    public static final int CAPITALIZE = 4;
    public static final int TRIM = 8;
    public static final int TRUNCATE_SILENTLY = 16;
    
    // Values coming from meta collection.
    public static final String LOWER_CASE_STR = "LOWER_CASE";
    public static final String UPPER_CASE_STR = "UPPER_CASE";
    public static final String CAPITALIZE_STR = "CAPITALIZE";
    public static final String TRIM_STR = "TRIM";
    public static final String TRUNCATE_SILENTLY_STR = "TRUNCATE_SILENTLY";
	
    /**
     * Used for converting numbers to strings.
     */
    public static final BigDecimal OFFSET_VALUE = new BigDecimal(Long.MIN_VALUE).negate();
    
	private String columnName;
	private boolean required;
	private int length;
	private int normalization; // Bit mask, to determine normalization rules.
	private ItemMetadata itemMetadata;
	
	/**
	 * 
	 */
	public AttributeMetadata() {
		setLazy(false);
	}

	/**
	 * @return
	 */
	public boolean isRequired() {
		return required;
	}
    
	/**
	 * @param required
	 */
	public void setRequired(String required) {
		this.required = new Boolean(required).booleanValue();
	}
    
    /**
     * @return
     */
    public int getLength() {
        return length;
    }
    
    /**
     * @param length
     */
    public void setLength(String length) {
        if(StringUtils.isNotEmpty(length)){
            this.length = Integer.parseInt(length);
        }
    }
    
	/**
	 * @return
	 */
	public int getNormalization() {
		return normalization;
	}
    
	/* (non-Javadoc)
	 * @see com.netflexitysolutions.amazonws.sdb.orm.metadata.AbstractSimpleDBMetadata#addMeta(java.lang.String, java.lang.String)
	 */
	@Override
	public void addMeta(String key, String value) {
		super.addMeta(key, value);
		
		// See if this is a normalization rules.
		addNormalizationRule(key);
	}

	/**
	 * @param rule
	 */
	protected void addNormalizationRule(String rule) {
		if(StringUtils.isBlank(rule)){
			throw new MetadataException("'attribute' is a required attribute of <meta>, which must be supplied for " + this.getClass().getName());
		}

		// Set normalization mask.
		if (LOWER_CASE_STR.equals(rule)){
            normalization |= LOWER_CASE;
		}
        else if (UPPER_CASE_STR.equals(rule)){
            normalization |= UPPER_CASE;
        }
        else if (CAPITALIZE_STR.equals(rule)){
            normalization |= CAPITALIZE;
        }
        else if (TRIM_STR.equals(rule)){
            normalization |= TRIM;
        }
        else if (TRUNCATE_SILENTLY_STR.equals(rule)) {
            normalization |= TRUNCATE_SILENTLY;
        }
	}
    
    /**
     * @param value
     * @return
     */
    public String normalize(String value){
        int normalization = getNormalization();
        if ((normalization & LOWER_CASE) != 0){
            value = value.toLowerCase();
        }
        if ((normalization & UPPER_CASE) != 0){
            value = value.toUpperCase();
        }
        if ((normalization & CAPITALIZE) != 0){
            value = StringUtils.capitalize(value);
        }
        if ((normalization & TRIM) != 0){
            value = value.trim();
        }
        if ((normalization & TRUNCATE_SILENTLY) != 0){
            if(value.length() > getLength()){
                value = value.substring(0, getLength());
            }
        }
        return value;
    }
    
    /**
     * @return
     */
    public ItemMetadata getItemMetadata() {
        return itemMetadata;
    }
    
    /**
	 * @param itemMetadata the itemMetadata to set
	 */
	public void setItemMetadata(ItemMetadata itemMetadata) {
		this.itemMetadata = itemMetadata;
	}
	
    /**
	 * @return the columnName
	 */
	public String getColumnName() {
		return columnName;
	}

	/**
	 * @param columnName the columnName to set
	 */
	public void setColumnName(String columnName) {
		this.columnName = columnName;
	}
	
	/**
	 * Retrieves bean property.
	 * 
	 * @param <T>
	 * @param record
	 * @return
	 */
	public <T> Object getFieldValue(T record) {
    	Object ob = null;
        try {
        	if(record != null){
        		ob = PropertyUtils.getProperty(record, getName());
        	}
        }
        catch (IllegalAccessException e) {
            logger.error("Failed to get property:" + getName() + " from record " + ((record != null) ? record.getClass().getName() : "") + ":" + e.getMessage(), e);
            assert(false);
        }
        catch (InvocationTargetException e) {
        	logger.error("Failed to get property:" + getName() + " from record " + ((record != null) ? record.getClass().getName() : "") + ":" + e.getMessage(), e);
            assert(false);
        }
        catch (NoSuchMethodException e) {
        	logger.error("Failed to get property:" + getName() + " from record " + ((record != null) ? record.getClass().getName() : "") + ":" + e.getMessage(), e);
            assert(false);
        }
        return ob;
	}
	
	/**
     * Changes bean property.
     * 
     * @param <T>
     * @param record
     * @param val
     */
    public <T> void setFieldValue(T record, Object value) {
        try {
        	if(record != null){
        		PropertyUtils.setProperty(record, getName(), value);
        	}
        }
        catch (IllegalAccessException e) {
        	logger.error("Failed to set property:" + getName() + " for record " + ((record != null) ? record.getClass().getName() : "") + ", with value '" + value + "':" + e.getMessage(), e);
            assert(false);
        }
        catch (InvocationTargetException e) {
            logger.error(e.getMessage(), e);
            assert(false);
        } 
        catch (NoSuchMethodException e) {
        	logger.error(e.getMessage(), e);
            assert(false);
		}
    }
    
    /**
     * This method is called by code that creates Amazon SimpleDB objects.
     * Encode value to Amazon format (if appropriate).
     * 
     * @param <T>
     * @param record
     * @return
     */
    public <T> String toSimpleDB(T record) {
    	Object ob = getFieldValue(record);
        if(ob == null) return null;
        
        return toSimpleDBValue(ob);
    }
    
    /**
     * Encode value to Amazon format (if appropriate).
     * 
     * @param <T>
     * @param record
     * @return
     */
    public <T> String toSimpleDBValue(Object ob) {
    	if(ob == null) return null;
        
        if (ob instanceof Integer || ob instanceof Long) {
            return AmazonSDBDataUtils.encodeRealNumberRange(new BigDecimal(ob.toString()), AmazonSDBDataUtils.LONG_DIGITS, OFFSET_VALUE);
        } 
        else if (ob instanceof Double || ob instanceof Float) {
            return AmazonSDBDataUtils.encodeRealNumberRange(new BigDecimal(ob.toString()), AmazonSDBDataUtils.LONG_DIGITS, AmazonSDBDataUtils.LONG_DIGITS,
                    OFFSET_VALUE);
        } 
        else if (ob instanceof BigDecimal) {
            return AmazonSDBDataUtils.encodeRealNumberRange((BigDecimal) ob, AmazonSDBDataUtils.LONG_DIGITS, AmazonSDBDataUtils.LONG_DIGITS,
                    OFFSET_VALUE);
        } 
        else if (ob instanceof Date) {
            return AmazonSDBDataUtils.encodeDate((Date) ob);
        }
        else if (ob instanceof Calendar) {
            return AmazonSDBDataUtils.encodeDate(((Calendar) ob).getTime());
        }
        else if(ob.getClass().isEnum()){
        	return ob.toString();
        }
    	else if (ob instanceof String) {
        	// Normalize the string.
        	String value = (String)ob;
        	if(StringUtils.isNotBlank(value)){
        		ob = normalize(value);
        	}
        }
        return String.valueOf(ob);
    }
    
    /**
     * This method is called by code that processes Amazon SimpleDB results and creates record instances.
     * Decode value from Amazon (if appropriate) and set into the record object.
     * 
     * @param <T>
     * @param record
     * @param val
     */
    public <T> void fromSimpleDB(T record, String val) {
    	Object value = val;
        if (Integer.class.isAssignableFrom(getJavaClass())) {
            value = AmazonSDBDataUtils.decodeRealNumberRange(val, OFFSET_VALUE).intValue();
        } 
        else if (Long.class.isAssignableFrom(getJavaClass())) {
        	value = AmazonSDBDataUtils.decodeRealNumberRange(val, OFFSET_VALUE).longValue();
        } 
        else if (Float.class.isAssignableFrom(getJavaClass())) {
        	value = AmazonSDBDataUtils.decodeRealNumberRange(val, AmazonSDBDataUtils.LONG_DIGITS, OFFSET_VALUE).floatValue();
        } 
        else if (Double.class.isAssignableFrom(getJavaClass())) {
        	value = AmazonSDBDataUtils.decodeRealNumberRange(val, AmazonSDBDataUtils.LONG_DIGITS, OFFSET_VALUE).doubleValue();
        } 
        else if (BigDecimal.class.isAssignableFrom(getJavaClass())) {
        	value = AmazonSDBDataUtils.decodeRealNumberRange(val, AmazonSDBDataUtils.LONG_DIGITS, OFFSET_VALUE);
        } 
        else if (Date.class.isAssignableFrom(getJavaClass())) {
            try {
            	value = AmazonSDBDataUtils.decodeDate(val);
            } 
            catch (Exception e) {
                logger.error(e);
            }
        }
        else if (Calendar.class.isAssignableFrom(getJavaClass())) {
            try {
            	Date date = AmazonSDBDataUtils.decodeDate(val);
            	Calendar calendar = Calendar.getInstance();
            	calendar.setTime(date);
            	value = calendar;
            } 
            catch (Exception e) {
                logger.error(e);
            }
        }
        else if(getJavaClass().isEnum()){
        	Object[] constants = getJavaClass().getEnumConstants();
        	for (Object object : constants) {
				if(object.toString().equals(val)){
					value = object;
					break;
				}
			}
        }
        setFieldValue(record, value);
    }

	/**
	 * @param required the required to set
	 */
	protected void setRequired(boolean required) {
		this.required = required;
	}

	/**
	 * @param length the length to set
	 */
	protected void setLength(int length) {
		this.length = length;
	}

	/**
	 * @param normalization the normalization to set
	 */
	protected void setNormalization(int normalization) {
		this.normalization = normalization;
	}
}
